<!DOCTYPE html>
        if (isDense) {
          // 生成可变的 rects 引用（与 placedRects 对应）
          const rects = placedRects.map(r => ({ x: r.x, y: r.y, w: r.w, h: r.h }));
          const iterations = 40;
          const maxShiftPerStep = 8; // 每次迭代最大移动像素，防止震荡

          for (let it = 0; it < iterations; it++) {
            let moved = false;
            for (let a = 0; a < rects.length; a++) {
              for (let b = a + 1; b < rects.length; b++) {
                const A = rects[a];
                const B = rects[b];
                if (rectsOverlap(A, B)) {
                  // 计算中心差
                  const Ax = A.x + A.w / 2;
                  const Ay = A.y + A.h / 2;
                  const Bx = B.x + B.w / 2;
                  const By = B.y + B.h / 2;

                  const dx = Ax - Bx;
                  const dy = Ay - By;

                  // 计算重叠量在 X/Y 方向
                  const overlapX = (A.w + B.w) / 2 - Math.abs(dx);
                  const overlapY = (A.h + B.h) / 2 - Math.abs(dy);

                  // 归一化方向
                  const dirX = dx === 0 ? (Math.random() > 0.5 ? 1 : -1) : dx / Math.abs(dx);
                  const dirY = dy === 0 ? (Math.random() > 0.5 ? 1 : -1) : dy / Math.abs(dy);

                  // 计算移动量：按重叠比例同时在 X/Y 推开
                  let shiftX = overlapX > 0 ? Math.ceil(overlapX / 2) * dirX : 0;
                  let shiftY = overlapY > 0 ? Math.ceil(overlapY / 2) * dirY : 0;

                  // 限制每步最大移动，避免过大跃迁
                  shiftX = Math.max(-maxShiftPerStep, Math.min(maxShiftPerStep, shiftX));
                  shiftY = Math.max(-maxShiftPerStep, Math.min(maxShiftPerStep, shiftY));

                  // 如果两个方向都为0（极小重叠），施加最小移动
                  if (shiftX === 0 && shiftY === 0) {
                    shiftY = dirY * 1;
                  }

                  // 将 A/B 按比例分开（A 移动 +, B 移动 -）
                  A.x += shiftX;
                  A.y += shiftY;
                  B.x -= shiftX;
                  B.y -= shiftY;

                  // 边界约束：不要移出图表区域
                  A.x = Math.max(chartRect.left, Math.min(chartRect.left + chartRect.width - A.w, A.x));
                  B.x = Math.max(chartRect.left, Math.min(chartRect.left + chartRect.width - B.w, B.x));
                  A.y = Math.max(chartRect.top, Math.min(chartRect.top + chartRect.height - A.h, A.y));
                  B.y = Math.max(chartRect.top, Math.min(chartRect.top + chartRect.height - B.h, B.y));

                  moved = true;
                }
              }
            }
            if (!moved) break;
          }

          // 将 rects 的最终位置回写到 updatedBarData/updatedLineData 的 label.position（按匹配序列）
          // 注意：placedRects 包含 bar 和 line 的交替添加顺序；我们只需更新已显示项
          let prIndex = 0;
          for (let i = 0; i < categories.length; i++) {
            if (isDense && !updatedIndicesToShow[i]) continue;
            // bar
            const barRect = rects[prIndex++];
            // 通过像素差计算新的 offset（相对于原始点）
            const barPxItem = barPx[i] || [0, 0];
            const newBarOffset = Math.round((barRect.y + barRect.h) - barPxItem[1]);
            // 更新对应的数据项
            const barItem = updatedBarData.find((d) => d.value === barData[i] && d.label && d.label.show);
            if (barItem) barItem.label.position = [0, newBarOffset];

            // line
            const lineRect = rects[prIndex++];
            const linePxItem = linePx[i] || [0, 0];
            const newLineOffset = Math.round((lineRect.y + lineRect.h) - linePxItem[1]);
            const lineItem = updatedLineData.find((d) => d.value === lineData[i] && d.label && d.label.show);
            if (lineItem) lineItem.label.position = [0, newLineOffset];
          }
        }
        padding: 20px;
        border-radius: 10px;
        margin-top: 20px;
      }
      .config-item {
        margin-bottom: 15px;
      }
      .config-item label {
        display: inline-block;
        width: 150px;
        font-weight: 600;
        color: #495057;
      }
      .btn {
        background: linear-gradient(45deg, #667eea, #764ba2);
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
        cursor: pointer;
        font-weight: 600;
        transition: all 0.3s ease;
        margin-right: 10px;
        margin-bottom: 10px;
      }
      .btn:hover {
        transform: translateY(-2px);
        box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
      }
      .stats {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 15px;
        margin-top: 20px;
      }
      .stat-card {
        background: white;
        padding: 15px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        text-align: center;
      }
      .stat-value {
        font-size: 1.5rem;
        font-weight: bold;
        color: #2c3e50;
      }
      .stat-label {
        font-size: 0.9rem;
        color: #7f8c8d;
      }
      .legend-adjust {
        color: #e74c3c;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="header">
        <h1>ECharts双Y轴图表防重叠优化</h1>
        <p>增强版智能避让算法 - 彻底解决标签重叠问题</p>
      </div>

      <div class="chart-container" id="mainChart"></div>

      <div class="stats">
        <div class="stat-card">
          <div class="stat-value" id="totalLabels">0</div>
          <div class="stat-label">总标签数</div>
        </div>
        <div class="stat-card">
          <div class="stat-value" id="adjustedLabels">0</div>
          <div class="stat-label">调整标签数</div>
        </div>
        <div class="stat-card">
          <div class="stat-value" id="overlapAvoided">0</div>
          <div class="stat-label">避免重叠数</div>
        </div>
      </div>

      <div class="control-panel">
        <h3>测试场景</h3>
        <div class="config-item">
          <button class="btn" onclick="loadTestData('normal')">
            <i class="fas fa-chart-bar"></i> 正常数据
          </button>
          <button class="btn" onclick="loadTestData('closeValues')">
            <i class="fas fa-compress-alt"></i> 相近数值
          </button>
          <button class="btn" onclick="loadTestData('mixed')">
            <i class="fas fa-random"></i> 混合数据
          </button>
          <button class="btn" onclick="loadTestData('extreme')">
            <i class="fas fa-exclamation-triangle"></i> 极端情况
          </button>
        </div>
      </div>
    </div>

    <script type="module">
      import { adjustLabelsAfterRender } from './labelLayout.js';
      let chart;

      // 渲染后基于像素的标签摆放算法：确保标签不遮挡柱子，且柱状与折线标签互不重叠
      // 算法思路：先快速渲染图表（初始标签位置），渲染完成后通过 convertToPixel 获取每个点的像素位置，
      // 然后在像素空间中尝试多个偏移候选（优先向上），避免与已有标签矩形重叠，同时保证不覆盖柱子顶部。
      function adjustLabelsAfterRender(categories, barData, lineData) {
        const chartRect = chart.getDom().getBoundingClientRect();
        const placedRects = []; // 已放置标签的像素矩形
        const fontSizeDefault = 11;

        const updatedBarData = [];
        const updatedLineData = [];

        // 辅助：基于 canvas.measureText 的精确文本尺寸测量
        const _textMeasureCanvas = document.createElement("canvas");
        const _textMeasureCtx = _textMeasureCanvas.getContext("2d");
        const _chartFontFamily =
          window.getComputedStyle(chart.getDom()).fontFamily || "sans-serif";

        function estimateTextSize(text, fontSize, fontWeight = "bold") {
          // 设置 canvas 字体样式以获得准确宽度
          _textMeasureCtx.font = `${fontWeight} ${fontSize}px ${_chartFontFamily}`;
          const metrics = _textMeasureCtx.measureText(text || "");
          // 加上少量内边距以避免紧贴
          const width = Math.ceil(metrics.width) + 8;
          const height = Math.ceil(fontSize) + 6;
          return { width, height };
        }

        // 矩形重叠检测
        function rectsOverlap(a, b) {
          return !(
            a.x + a.w < b.x ||
            a.x > b.x + b.w ||
            a.y + a.h < b.y ||
            a.y > b.y + b.h
          );
        }

        // 先收集所有像素位置，以便判断密集度并决定展示策略
        const barPx = [];
        const linePx = [];
        for (let i = 0; i < categories.length; i++) {
          try {
            barPx[i] = chart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 0 }, [
              categories[i],
              barData[i],
            ]);
            linePx[i] = chart.convertToPixel({ xAxisIndex: 0, yAxisIndex: 1 }, [
              categories[i],
              lineData[i],
            ]);
          } catch (e) {
            barPx[i] = [0, 0];
            linePx[i] = [0, 0];
          }
        }

        // 计算平均类目间距（像素）作为密集度判定依据
        let avgSpacing = 0;
        if (barPx.length >= 2) {
          let sum = 0;
          let count = 0;
          for (let i = 1; i < barPx.length; i++) {
            const dx = Math.abs(barPx[i][0] - barPx[i - 1][0]);
            if (dx > 0) {
              sum += dx;
              count++;
            }
          }
          avgSpacing = count ? sum / count : 0;
        }

        // 估算平均标签宽度（用画布测量当前字体）
        const sampleText = `${Math.max(...barData)}万元`;
        const sampleSize = estimateTextSize(sampleText, fontSizeDefault);
        const avgLabelWidth = sampleSize.width;

        // 判定为密集数据时的阈值（经验值）
        const isDense = avgSpacing > 0 && avgSpacing < avgLabelWidth * 0.9;

        // 当密集时，采用混合策略：抽样展示与强力迭代推斥
        const updatedIndicesToShow = new Array(categories.length).fill(true);
        if (isDense) {
          // 计算步长（每 showStep 个展示一个），确保最大值/拐点始终展示
          const showStep = Math.max(1, Math.round(avgLabelWidth / avgSpacing));
          // 找到 topK 的重要索引（例如顶部3个）保证显示
          const topK = 3;
          const barSortedIdx = barData
            .map((v, idx) => ({ v, idx }))
            .sort((a, b) => b.v - a.v)
            .slice(0, topK)
            .map((x) => x.idx);
          const lineSortedIdx = lineData
            .map((v, idx) => ({ v, idx }))
            .sort((a, b) => b.v - a.v)
            .slice(0, topK)
            .map((x) => x.idx);

          for (let i = 0; i < categories.length; i++) {
            if (barSortedIdx.includes(i) || lineSortedIdx.includes(i)) {
              updatedIndicesToShow[i] = true;
            } else if (i % showStep !== 0) {
              updatedIndicesToShow[i] = false; // 抽样隐藏
            }
          }
        }

        // 为每个索引选择一个合适的偏移
        for (let i = 0; i < categories.length; i++) {
          // 获取像素坐标（x, y）: 对于类目轴，convertToPixel 支持 [category, value]
          const barPosPx = barPx[i] || [0, 0];
          const linePosPx = linePx[i] || [0, 0];

          // 如果被抽样隐藏，则直接返回隐藏配置
          if (isDense && !updatedIndicesToShow[i]) {
            updatedBarData.push({ value: barData[i], label: { show: false } });
            updatedLineData.push({
              value: lineData[i],
              label: { show: false },
            });
            continue;
          }

          // 估算标签尺寸
          const barText = `${barData[i]}万元`;
          const lineText = `${lineData[i]}%`;
          let barFont = fontSizeDefault;
          let lineFont = fontSizeDefault;
          let barSize = estimateTextSize(barText, barFont, "bold");
          let lineSize = estimateTextSize(lineText, lineFont, "bold");

          // 生成候选偏移（像素），优先负向（上方），然后向右/左/下方尝试
          const candidates = [-8, -24, -40, -56, 8, 24, 40, 56];

          // 确保标签不与柱子顶部重叠：计算柱子顶部像素 y 值已由 barPosPx[1]
          const barTopY = barPosPx[1];

          // 先放置柱状标签
          let chosenBarOffset = null;
          for (const dy of candidates) {
            // 对柱状标签，我们更希望它位于柱子上方（dy 负值）
            const offsetY = dy;
            const rect = {
              x: barPosPx[0] - barSize.width / 2,
              y: barPosPx[1] + offsetY - barSize.height,
              w: barSize.width,
              h: barSize.height,
            };

            // 不允许覆盖柱子顶部（保证 rect.y + rect.h <= barTopY - 4）
            if (rect.y + rect.h > barTopY - 4) {
              // 如果候选位于柱子区域，则跳过
              continue;
            }

            // 检查是否与已有标签重叠
            let collide = false;
            for (const r of placedRects) {
              if (rectsOverlap(rect, r)) {
                collide = true;
                break;
              }
            }
            if (!collide) {
              chosenBarOffset = offsetY;
              placedRects.push(rect);
              break;
            }
          }

          if (chosenBarOffset === null) {
            // 无合适位置：尝试缩小字体并重试一次
            barFont = Math.max(9, fontSizeDefault - 2);
            barSize = estimateTextSize(barText, barFont);
            for (const dy of candidates) {
              const rect = {
                x: barPosPx[0] - barSize.width / 2,
                y: barPosPx[1] + dy - barSize.height,
                w: barSize.width,
                h: barSize.height,
              };
              if (rect.y + rect.h > barTopY - 2) continue;
              let collide = false;
              for (const r of placedRects) {
                if (rectsOverlap(rect, r)) {
                  collide = true;
                  break;
                }
              }
              if (!collide) {
                chosenBarOffset = dy;
                placedRects.push(rect);
                break;
              }
            }
          }

          // 如果依然没有位置，最后选择放在柱子顶部外侧（尽量不覆盖，但标记为调整）
          if (chosenBarOffset === null) {
            chosenBarOffset = -8;
            // 仍然记录一个矩形，允许轻微重叠
            placedRects.push({
              x: barPosPx[0] - barSize.width / 2,
              y: barPosPx[1] + chosenBarOffset - barSize.height,
              w: barSize.width,
              h: barSize.height,
            });
          }

          // 再放置折线标签（优先不与柱状标签重叠）
          const lineCandidates = [-8, -24, -40, 8, 24, 40];
          let chosenLineOffset = null;
          for (const dy of lineCandidates) {
            const rect = {
              x: linePosPx[0] - lineSize.width / 2,
              y: linePosPx[1] + dy - lineSize.height,
              w: lineSize.width,
              h: lineSize.height,
            };

            // 检查与已有标签（包含刚放的柱状标签）重叠
            let collide = false;
            for (const r of placedRects) {
              if (rectsOverlap(rect, r)) {
                collide = true;
                break;
              }
            }
            if (!collide) {
              chosenLineOffset = dy;
              placedRects.push(rect);
              break;
            }
          }

          if (chosenLineOffset === null) {
            // 缩小字体并重试
            lineFont = Math.max(9, fontSizeDefault - 2);
            lineSize = estimateTextSize(lineText, lineFont);
            for (const dy of lineCandidates) {
              const rect = {
                x: linePosPx[0] - lineSize.width / 2,
                y: linePosPx[1] + dy - lineSize.height,
                w: lineSize.width,
                h: lineSize.height,
              };
              let collide = false;
              for (const r of placedRects) {
                if (rectsOverlap(rect, r)) {
                  collide = true;
                  break;
                }
              }
              if (!collide) {
                chosenLineOffset = dy;
                placedRects.push(rect);
                break;
              }
            }
          }

          if (chosenLineOffset === null) {
            chosenLineOffset = -8; // 最终兜底
            placedRects.push({
              x: linePosPx[0] - lineSize.width / 2,
              y: linePosPx[1] + chosenLineOffset - lineSize.height,
              w: lineSize.width,
              h: lineSize.height,
            });
          }

          // 构建更新后的数据项，使用 position 数组作为像素偏移（基于 data point）
          updatedBarData.push({
            value: barData[i],
            label: {
              show: true,
              position: [0, chosenBarOffset],
              formatter: `${barData[i]}万元`,
              textStyle: {
                fontSize: barFont,
                color: "#2c3e50",
              },
            },
          });

          updatedLineData.push({
            value: lineData[i],
            label: {
              show: true,
              position: [0, chosenLineOffset],
              formatter: `${lineData[i]}%`,
              textStyle: { fontSize: lineFont, color: "#e74c3c" },
            },
          });
        }

        // 对已放置的标签做一次迭代式推斥（仅限密集场景或当有重叠时）以进一步消除残余重叠
        if (isDense) {
          // 生成可变的 rects 引用（与 placedRects 对应）
          const rects = placedRects.slice();
          const iterations = 30;
          for (let it = 0; it < iterations; it++) {
            let moved = false;
            for (let a = 0; a < rects.length; a++) {
              for (let b = a + 1; b < rects.length; b++) {
                const A = rects[a];
                const B = rects[b];
                if (rectsOverlap(A, B)) {
                  // 垂直分离为主：根据中心位置推开
                  const Ay = A.y + A.h / 2;
                  const By = B.y + B.h / 2;
                  const overlapY = (A.h + B.h) / 2 - Math.abs(Ay - By);
                  if (overlapY > 0) {
                    const shift = Math.ceil(overlapY / 2) + 1;
                    if (Ay <= By) {
                      A.y -= shift;
                      B.y += shift;
                    } else {
                      A.y += shift;
                      B.y -= shift;
                    }
                    moved = true;
                  }
                }
              }
            }
            if (!moved) break;
          }

          // 将 rects 的最终位置回写到 updatedBarData/updatedLineData 的 label.position（按匹配序列）
          // 注意：placedRects 包含 bar 和 line 的交替添加顺序；我们只需更新已显示项
          let prIndex = 0;
          for (let i = 0; i < categories.length; i++) {
            if (isDense && !updatedIndicesToShow[i]) continue;
            // bar
            const barRect = rects[prIndex++];
            // 通过像素差计算新的 offset（相对于原始点）
            const barPxItem = barPx[i] || [0, 0];
            const newBarOffset = Math.round(
              barRect.y + barRect.h - barPxItem[1]
            );
            // 更新对应的数据项
            const barItem = updatedBarData.find(
              (d) => d.value === barData[i] && d.label && d.label.show
            );
            if (barItem) barItem.label.position = [0, newBarOffset];

            // line
            const lineRect = rects[prIndex++];
            const linePxItem = linePx[i] || [0, 0];
            const newLineOffset = Math.round(
              lineRect.y + lineRect.h - linePxItem[1]
            );
            const lineItem = updatedLineData.find(
              (d) => d.value === lineData[i] && d.label && d.label.show
            );
            if (lineItem) lineItem.label.position = [0, newLineOffset];
          }
        }

        return { updatedBarData, updatedLineData };
      }

      // 初始化图表
      function initChart() {
        const chartDom = document.getElementById("mainChart");
        chart = echarts.init(chartDom);

        // 默认测试数据
        loadTestData("normal");

        window.addEventListener("resize", function () {
          chart.resize();
        });
      }

      // 渲染图表：先快速渲染，再基于像素调整标签位置以避免重叠和遮挡
      function renderChart(categories, barData, lineData) {
        // 初始渲染，使用简单标签位置（随后会被调整）
        const option = {
          title: {
            text: "双Y轴图表 - 增强版智能防重叠",
            left: "center",
            textStyle: {
              color: "#2c3e50",
              fontSize: 18,
              fontWeight: "bold",
            },
          },
          tooltip: {
            trigger: "axis",
            axisPointer: {
              type: "shadow",
            },
          },
          legend: {
            data: ["销售额", "增长率"],
            top: 30,
            textStyle: {
              fontSize: 12,
            },
          },
          grid: {
            left: "3%",
            right: "4%",
            bottom: "10%",
            top: "18%",
            containLabel: true,
          },
          xAxis: {
            type: "category",
            data: categories,
            axisLabel: {
              interval: 0,
              rotate: 0,
              fontSize: 12,
              color: "#666",
            },
            axisLine: {
              lineStyle: {
                color: "#e0e0e0",
              },
            },
          },
          yAxis: [
            {
              type: "value",
              name: "销售额（万元）",
              position: "left",
              axisLine: {
                lineStyle: {
                  color: "#3498db",
                },
              },
              splitLine: {
                lineStyle: {
                  color: "#f0f0f0",
                },
              },
            },
            {
              type: "value",
              name: "增长率（%）",
              position: "right",
              axisLine: {
                lineStyle: {
                  color: "#e74c3c",
                },
              },
              splitLine: {
                show: false,
              },
            },
          ],
          series: [
            {
              name: "销售额",
              type: "bar",
              barWidth: "40%",
              barCategoryGap: "5%",
              yAxisIndex: 0,
              data: barData.map((v) => ({
                value: v,
                label: { show: true, position: [0, -8], formatter: `${v}万元` },
              })),
              itemStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                  { offset: 0, color: "#83bff6" },
                  { offset: 1, color: "#188df0" },
                ]),
                borderRadius: [6, 6, 0, 0],
              },
            },
            {
              name: "增长率",
              type: "line",
              yAxisIndex: 1,
              symbol: "circle",
              symbolSize: 10,
              data: lineData.map((v) => ({
                value: v,
                label: { show: true, position: [0, -8], formatter: `${v}%` },
              })),
              lineStyle: {
                color: "#e74c3c",
                width: 3,
              },
              itemStyle: {
                color: "#e74c3c",
                borderWidth: 2,
                borderColor: "#ffffff",
              },
            },
          ],
        };

        chart.setOption(option);

  // 等待渲染完成后基于像素进行标签布局调整
  setTimeout(async () => {
          // 更新统计前先计算新的标签摆放
          const { updatedBarData, updatedLineData } = await adjustLabelsAfterRender(
            chart,
            categories,
            barData,
            lineData
          );

          // 更新统计信息
          const totalLabels = categories.length * 2;
          // 统计被实际调整的数量：与初始偏移 -8 比较
          const adjustedLabels =
            updatedBarData.filter((d) => d.label.position[1] !== -8).length +
            updatedLineData.filter((d) => d.label.position[1] !== -8).length;
          const overlapAvoided = adjustedLabels;

          document.getElementById("totalLabels").textContent = totalLabels;
          document.getElementById("adjustedLabels").textContent =
            adjustedLabels;
          document.getElementById("overlapAvoided").textContent =
            overlapAvoided;

          // 将调整后的 label 配置应用到图表
          chart.setOption({
            series: [{ data: updatedBarData }, { data: updatedLineData }],
          });
        }, 60);
      }

      // 加载测试数据
      function loadTestData(type) {
        let categories, barData, lineData;

        switch (type) {
          case "normal":
            (categories = ["1月", "2月", "3月", "4月", "5月", "6月"]),
              (barData = [120, 200, 150, 80, 70, 110]),
              (lineData = [10, 20, 15, 5, 8, 12]);
            break;
          case "closeValues":
            (categories = ["1月", "2月", "3月", "4月", "5月"]),
              (barData = [250, 248, 252, 249, 251]),
              (lineData = [12, 11, 13, 10, 12]);
            break;
          case "mixed":
            (categories = ["周一", "周二", "周三", "周四", "周五"]),
              (barData = [180, 190, 185, 192, 188]),
              (lineData = [8, 9, 7, 10, 8]);
            break;
          case "extreme":
            (categories = ["产品A", "产品B", "产品C", "产品D", "产品E"]),
              (barData = [300, 298, 302, 299, 301]),
              (lineData = [15, 14, 16, 13, 15]);
            break;
        }

        renderChart(categories, barData, lineData);
      }

      // 页面加载完成后初始化图表
      document.addEventListener("DOMContentLoaded", function () {
        initChart();
      });
    </script>
  </body>
</html>
